/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.tests.sigtest;

import android.content.res.Resources;
import android.test.AndroidTestCase;
import android.tests.sigtest.JDiffClassDescription.JDiffConstructor;
import android.tests.sigtest.JDiffClassDescription.JDiffField;
import android.tests.sigtest.JDiffClassDescription.JDiffMethod;
import android.tests.sigtest.SignatureTestActivity.FAILURE_TYPE;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashSet;

/**
 * A simpler version of {@link SignatureTest} that performs the signature check via a JUnit test.
 * <p/>
 * Eventually the existing {@link SignatureTest} and {@link SignatureTestActivity} will be deleted
 * once the move to a tradefederation based CTS harness is complete.
 */
public class RcsApiSignatureTest extends AndroidTestCase {

    private static final String TAG_ROOT = "api";
    private static final String TAG_PACKAGE = "package";
    private static final String TAG_CLASS = "class";
    private static final String TAG_INTERFACE = "interface";
    private static final String TAG_IMPLEMENTS = "implements";
    private static final String TAG_CONSTRUCTOR = "constructor";
    private static final String TAG_METHOD = "method";
    private static final String TAG_PARAM = "parameter";
    private static final String TAG_EXCEPTION = "exception";
    private static final String TAG_FIELD = "field";

    private static final String MODIFIER_ABSTRACT = "abstract";
    private static final String MODIFIER_FINAL = "final";
    private static final String MODIFIER_NATIVE = "native";
    private static final String MODIFIER_PRIVATE = "private";
    private static final String MODIFIER_PROTECTED = "protected";
    private static final String MODIFIER_PUBLIC = "public";
    private static final String MODIFIER_STATIC = "static";
    private static final String MODIFIER_SYNCHRONIZED = "synchronized";
    private static final String MODIFIER_TRANSIENT = "transient";
    private static final String MODIFIER_VOLATILE = "volatile";
    private static final String MODIFIER_VISIBILITY = "visibility";

    private static final String ATTRIBUTE_NAME = "name";
    private static final String ATTRIBUTE_EXTENDS = "extends";
    private static final String ATTRIBUTE_TYPE = "type";
    private static final String ATTRIBUTE_RETURN = "return";

    private HashSet<String> mKeyTagSet;
    private TestResultObserver mResultObserver;

    private class TestResultObserver implements ResultObserver {
        boolean mDidFail = false;
        StringBuilder mErrorString = new StringBuilder();

        public void notifyFailure(FAILURE_TYPE type, String name, String errorMessage) {
            mDidFail = true;
            mErrorString.append("\n");
            mErrorString.append(type.toString().toLowerCase());
            mErrorString.append(":\t");
            mErrorString.append(name);
        }
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mKeyTagSet = new HashSet<>();
        mKeyTagSet.addAll(Arrays.asList(TAG_PACKAGE, TAG_CLASS, TAG_INTERFACE, TAG_IMPLEMENTS,
                TAG_CONSTRUCTOR, TAG_METHOD, TAG_PARAM, TAG_EXCEPTION, TAG_FIELD));
        mResultObserver = new TestResultObserver();
    }

    /**
     * Tests that the device's API matches the expected set defined in xml.
     * <p/>
     * Will check the entire API, and then report the complete list of failures
     */
    public void testSignature() {
        Resources r = getContext().getResources();
        try {
            start(r.getXml(ToTest.VERSION));
        } catch (Exception e) {
            mResultObserver.notifyFailure(FAILURE_TYPE.CAUGHT_EXCEPTION, e.getMessage(),
                    e.getMessage());
        }
        if (mResultObserver.mDidFail) {
            fail(mResultObserver.mErrorString.toString());
        }
    }

    private void beginDocument(XmlPullParser parser, String firstElementName)
            throws XmlPullParserException, IOException {
        int type;
        while ((type = parser.next()) != XmlPullParser.START_TAG
                && type != XmlPullParser.END_DOCUMENT) {
        }

        if (type != XmlPullParser.START_TAG) {
            throw new XmlPullParserException("No start tag found");
        }

        if (!parser.getName().equals(firstElementName)) {
            throw new XmlPullParserException("Unexpected start tag: found " + parser.getName()
                    + ", expected " + firstElementName);
        }
    }

    /**
     * Signature test entry point.
     */
    private void start(XmlPullParser parser) throws XmlPullParserException, IOException {
        JDiffClassDescription currentClass = null;
        String currentPackage = "";
        JDiffMethod currentMethod = null;

        beginDocument(parser, TAG_ROOT);
        int type;
        while (true) {
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.END_TAG) {

            }

            if (type == XmlPullParser.END_TAG) {
                if (TAG_CLASS.equals(parser.getName()) || TAG_INTERFACE.equals(parser.getName())) {
                    currentClass.checkSignatureCompliance();
                } else if (TAG_PACKAGE.equals(parser.getName())) {
                    currentPackage = "";
                }
                continue;
            }

            if (type == XmlPullParser.END_DOCUMENT) {
                break;
            }

            String tagname = parser.getName();
            if (!mKeyTagSet.contains(tagname)) {
                continue;
            }

            switch (tagname) {
                case TAG_PACKAGE:
                    currentPackage = parser.getAttributeValue(null, ATTRIBUTE_NAME);
                    break;
                case TAG_CLASS:
                    currentClass = loadClassInfo(parser, false, currentPackage);
                    break;
                case TAG_INTERFACE:
                    currentClass = loadClassInfo(parser, true, currentPackage);
                    break;
                case TAG_IMPLEMENTS:
                    currentClass.addImplInterface(parser.getAttributeValue(null, ATTRIBUTE_NAME));
                    break;
                case TAG_CONSTRUCTOR:
                    JDiffConstructor constructor = loadConstructorInfo(parser, currentClass);
                    currentClass.addConstructor(constructor);
                    currentMethod = constructor;
                    break;
                case TAG_METHOD:
                    currentMethod = loadMethodInfo(currentClass.getClassName(), parser);
                    currentClass.addMethod(currentMethod);
                    break;
                case TAG_PARAM:
                    currentMethod.addParam(parser.getAttributeValue(null, ATTRIBUTE_TYPE));
                    break;
                case TAG_EXCEPTION:
                    currentMethod.addException(parser.getAttributeValue(null, ATTRIBUTE_TYPE));
                    break;
                case TAG_FIELD:
                    JDiffField field = loadFieldInfo(currentClass.getClassName(), parser);
                    currentClass.addField(field);
                    break;
                default:
                    throw new RuntimeException("unknow tag exception:" + tagname);
            }
        }
    }

    /**
     * Load field information from xml to memory.
     * 
     * @param className of the class being examined which will be shown in error messages
     * @param parser The XmlPullParser which carries the xml information.
     * @return the new field
     */
    private JDiffField loadFieldInfo(String className, XmlPullParser parser) {
        String fieldName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
        String fieldType = parser.getAttributeValue(null, ATTRIBUTE_TYPE);
        int modifier = jdiffModifierToReflectionFormat(className, parser);
        return new JDiffField(fieldName, fieldType, modifier);
    }

    /**
     * Load method information from xml to memory.
     * 
     * @param className of the class being examined which will be shown in error messages
     * @param parser The XmlPullParser which carries the xml information.
     * @return the newly loaded method.
     */
    private JDiffMethod loadMethodInfo(String className, XmlPullParser parser) {
        String methodName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
        String returnType = parser.getAttributeValue(null, ATTRIBUTE_RETURN);
        int modifier = jdiffModifierToReflectionFormat(className, parser);
        return new JDiffMethod(methodName, modifier, returnType);
    }

    /**
     * Load constructor information from xml to memory.
     * 
     * @param parser The XmlPullParser which carries the xml information.
     * @param currentClass the current class being loaded.
     * @return the new constructor
     */
    private JDiffConstructor loadConstructorInfo(XmlPullParser parser,
            JDiffClassDescription currentClass) {
        String name = currentClass.getClassName();
        int modifier = jdiffModifierToReflectionFormat(name, parser);
        return new JDiffConstructor(name, modifier);
    }

    /**
     * Load class or interface information to memory.
     * 
     * @param parser The XmlPullParser which carries the xml information.
     * @param isInterface true if the current class is an interface, otherwise is false.
     * @param pkg the name of the java package this class can be found in.
     * @return the new class description.
     */
    private JDiffClassDescription loadClassInfo(XmlPullParser parser, boolean isInterface,
            String pkg) {
        String className = parser.getAttributeValue(null, ATTRIBUTE_NAME);
        JDiffClassDescription currentClass = new JDiffClassDescription(pkg, className,
                mResultObserver);
        currentClass.setModifier(jdiffModifierToReflectionFormat(className, parser));
        currentClass.setType(isInterface ? JDiffClassDescription.JDiffType.INTERFACE
                : JDiffClassDescription.JDiffType.CLASS);
        currentClass.setExtendsClass(parser.getAttributeValue(null, ATTRIBUTE_EXTENDS));
        return currentClass;
    }

    /**
     * Convert string modifier to int modifier.
     * 
     * @param name of the class/method/field being examined which will be shown in error messages
     * @param key modifier name
     * @param value modifier value
     * @return converted modifier value
     */
    private static int modifierDescriptionToReflectedType(String name, String key, String value) {
        switch (key) {
            case MODIFIER_ABSTRACT:
                return value.equals("true") ? Modifier.ABSTRACT : 0;
            case MODIFIER_FINAL:
                return value.equals("true") ? Modifier.FINAL : 0;
            case MODIFIER_NATIVE:
                return value.equals("true") ? Modifier.NATIVE : 0;
            case MODIFIER_STATIC:
                return value.equals("true") ? Modifier.STATIC : 0;
            case MODIFIER_SYNCHRONIZED:
                return value.equals("true") ? Modifier.SYNCHRONIZED : 0;
            case MODIFIER_TRANSIENT:
                return value.equals("true") ? Modifier.TRANSIENT : 0;
            case MODIFIER_VOLATILE:
                return value.equals("true") ? Modifier.VOLATILE : 0;
            case MODIFIER_VISIBILITY:
                switch (value) {
                    case MODIFIER_PRIVATE:
                        throw new RuntimeException("Private visibility found in API spec: " + name);
                    case MODIFIER_PROTECTED:
                        return Modifier.PROTECTED;
                    case MODIFIER_PUBLIC:
                        return Modifier.PUBLIC;
                    case "":
                        // If the visibility is "", it means it has no modifier.
                        // which is package private. We should return 0 for this modifier.
                        return 0;
                    default:
                        throw new RuntimeException("Unknown modifier found in API spec: " + value);
                }
        }
        return 0;
    }

    /**
     * Transfer string modifier to int one.
     * 
     * @param name of the class/method/field being examined which will be shown in error messages
     * @param parser XML resource parser
     * @return converted modifier
     */
    private static int jdiffModifierToReflectionFormat(String name, XmlPullParser parser) {
        int modifier = 0;
        for (int i = 0; i < parser.getAttributeCount(); i++) {
            modifier |= modifierDescriptionToReflectedType(name, parser.getAttributeName(i),
                    parser.getAttributeValue(i));
        }
        return modifier;
    }
}
